/*-------------------------------------------------------------------------
 *
 * gistxlog.c
 *	  GiST的WAL重放逻辑。
 *
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * IDENTIFICATION
 *			 src/backend/access/gist/gistxlog.c
 *-------------------------------------------------------------------------
 */
#include "postgres.h"

#include "access/bufmask.h"
#include "access/gist_private.h"
#include "access/gistxlog.h"
#include "access/heapam_xlog.h"
#include "access/transam.h"
#include "access/xloginsert.h"
#include "access/xlogutils.h"
#include "miscadmin.h"
#include "storage/procarray.h"
#include "utils/memutils.h"
#include "utils/rel.h"

static MemoryContext opCtx;		/* 工作内存用于操作 */

/*
 * 重放在子页面上清除 F_FOLLOW_RIGHT 标志的操作。
 *
 * 即使 WAL 记录包含完整页面映像，我们也必须更新
 * follow-right 标志，因为该更改未包含在完整页面
 * 映像中。为了确保具有错误标志值的中间状态
 * 不会对并发的 Hot Standby 查询可见，此函数处理
 * 恢复完整页面映像以及更新标志。（请注意，
 * 在当前的 WAL 操作中，我们从不需要对子页面进行
 * 其他任何操作。）
 */
static void fc_gistRedoClearFollowRight(XLogReaderState *fc_record, uint8 fc_block_id)
{
	XLogRecPtr	fc_lsn = fc_record->EndRecPtr;
	Buffer		fc_buffer;
	Page		fc_page;
	XLogRedoAction fc_action;

	/*
	 * 请注意，即使从完整页面映像恢复，我们仍然更新页面，
	 * 因为更新后的 NSN 不包含在图像中。
	 */
	fc_action = XLogReadBufferForRedo(fc_record, fc_block_id, &fc_buffer);
	if (fc_action == BLK_NEEDS_REDO || fc_action == BLK_RESTORED)
	{
		fc_page = BufferGetPage(fc_buffer);

		GistPageSetNSN(fc_page, fc_lsn);
		GistClearFollowRight(fc_page);

		PageSetLSN(fc_page, fc_lsn);
		MarkBufferDirty(fc_buffer);
	}
	if (BufferIsValid(fc_buffer))
		UnlockReleaseBuffer(fc_buffer);
}

/*
 * 重做任何页面更新（页面拆分除外）
 */
static void fc_gistRedoPageUpdateRecord(XLogReaderState *fc_record)
{
	XLogRecPtr	fc_lsn = fc_record->EndRecPtr;
	gistxlogPageUpdate *fc_xldata = (gistxlogPageUpdate *) XLogRecGetData(fc_record);
	Buffer		fc_buffer;
	Page		fc_page;

	if (XLogReadBufferForRedo(fc_record, 0, &fc_buffer) == BLK_NEEDS_REDO)
	{
		char	   *fc_begin;
		char	   *fc_data;
		Size		fc_datalen;
		int			fc_ninserted PG_USED_FOR_ASSERTS_ONLY = 0;

		fc_data = fc_begin = XLogRecGetBlockData(fc_record, 0, &fc_datalen);

		fc_page = (Page) BufferGetPage(fc_buffer);

		if (fc_xldata->ntodelete == 1 && fc_xldata->ntoinsert == 1)
		{
			/*
			 * 替换一个元组为另一个元组时，我们必须使用
			 * PageIndexTupleOverwrite 以与 gistplacetopage 保持一致。
			 */
			OffsetNumber fc_offnum = *((OffsetNumber *) fc_data);
			IndexTuple	fc_itup;
			Size		fc_itupsize;

			fc_data += sizeof(OffsetNumber);
			fc_itup = (IndexTuple) fc_data;
			fc_itupsize = IndexTupleSize(fc_itup);
			if (!PageIndexTupleOverwrite(fc_page, fc_offnum, (Item) fc_itup, fc_itupsize))
				elog(ERROR, "failed to add item to GiST index page, size %d bytes",
					 (int) fc_itupsize);
			fc_data += fc_itupsize;
			/* 消耗 1 个元组后不应该有剩余 */
			Assert(fc_data - fc_begin == fc_datalen);
			/* 更新下面断言检查的插入计数 */
			fc_ninserted++;
		}
		else if (fc_xldata->ntodelete > 0)
		{
			/* 否则，如果有旧元组则删除它们 */
			OffsetNumber *fc_todelete = (OffsetNumber *) fc_data;

			fc_data += sizeof(OffsetNumber) * fc_xldata->ntodelete;

			PageIndexMultiDelete(fc_page, fc_todelete, fc_xldata->ntodelete);
			if (GistPageIsLeaf(fc_page))
				GistMarkTuplesDeleted(fc_page);
		}

		/* 如果有新元组则添加它们 */
		if (fc_data - fc_begin < fc_datalen)
		{
			OffsetNumber fc_off = (PageIsEmpty(fc_page)) ? FirstOffsetNumber :
			OffsetNumberNext(PageGetMaxOffsetNumber(fc_page));

			while (fc_data - fc_begin < fc_datalen)
			{
				IndexTuple	fc_itup = (IndexTuple) fc_data;
				Size		fc_sz = IndexTupleSize(fc_itup);
				OffsetNumber fc_l;

				fc_data += fc_sz;

				fc_l = PageAddItem(fc_page, (Item) fc_itup, fc_sz, fc_off, false, false);
				if (fc_l == InvalidOffsetNumber)
					elog(ERROR, "failed to add item to GiST index page, size %d bytes",
						 (int) fc_sz);
				fc_off++;
				fc_ninserted++;
			}
		}

		/* 检查 XLOG 记录是否包含预期数量的元组 */
		Assert(fc_ninserted == fc_xldata->ntoinsert);

		PageSetLSN(fc_page, fc_lsn);
		MarkBufferDirty(fc_buffer);
	}

	/*
	 * 修正左子页面的 follow-right 数据
	 *
	 * 这必须在仍然持有目标页面锁时完成。请注意，
	 * 即使目标页面不复存在，我们仍然尝试在子页面上
	 * 重放更改。
	 */
	if (XLogRecHasBlockRef(fc_record, 1))
		fc_gistRedoClearFollowRight(fc_record, 1);

	if (BufferIsValid(fc_buffer))
		UnlockReleaseBuffer(fc_buffer);
}


/*
 * 在 gist 索引页面上重做删除，以移除在索引
 * 元组插入期间标记为 DEAD 的元组
 */
static void fc_gistRedoDeleteRecord(XLogReaderState *fc_record)
{
	XLogRecPtr	fc_lsn = fc_record->EndRecPtr;
	gistxlogDelete *fc_xldata = (gistxlogDelete *) XLogRecGetData(fc_record);
	Buffer		fc_buffer;
	Page		fc_page;

	/*
	 * 如果我们需要处理任何冲突，它必须在我们
	 * 更新页面之前进行。
	 *
	 * GiST 删除记录可能与待机查询发生冲突。您可能会认为
	 * 清理记录也会发生冲突，但我们已经处理过了。
	 * XLOG_HEAP2_PRUNE 记录提供了由堆的清理
	 * vacuum 清理的最高 xid，因此我们可以在
	 * 它到达时只解决一次任何冲突。之后，我们知道
	 * 在该索引上没有来自单个 gist vacuum 记录的冲突。
	 */
	if (InHotStandby)
	{
		RelFileNode fc_rnode;

		XLogRecGetBlockTag(fc_record, 0, &fc_rnode, NULL, NULL);

		ResolveRecoveryConflictWithSnapshot(fc_xldata->latestRemovedXid, fc_rnode);
	}

	if (XLogReadBufferForRedo(fc_record, 0, &fc_buffer) == BLK_NEEDS_REDO)
	{
		fc_page = (Page) BufferGetPage(fc_buffer);

		if (XLogRecGetDataLen(fc_record) > SizeOfGistxlogDelete)
		{
			OffsetNumber *fc_todelete;

			fc_todelete = (OffsetNumber *) ((char *) fc_xldata + SizeOfGistxlogDelete);

			PageIndexMultiDelete(fc_page, fc_todelete, fc_xldata->ntodelete);
		}

		GistClearPageHasGarbage(fc_page);
		GistMarkTuplesDeleted(fc_page);

		PageSetLSN(fc_page, fc_lsn);
		MarkBufferDirty(fc_buffer);
	}

	if (BufferIsValid(fc_buffer))
		UnlockReleaseBuffer(fc_buffer);
}

/*
 * 返回一个索引指针数组。
 */
static IndexTuple *
fc_decodePageSplitRecord(char *fc_begin, int fc_len, int *fc_n)
{
	char	   *fc_ptr;
	int			fc_i = 0;
	IndexTuple *fc_tuples;

	/* 提取元组的数量 */
	memcpy(fc_n, fc_begin, sizeof(int));
	fc_ptr = fc_begin + sizeof(int);

	fc_tuples = palloc(*fc_n * sizeof(IndexTuple));

	for (fc_i = 0; fc_i < *fc_n; fc_i++)
	{
		Assert(fc_ptr - fc_begin < fc_len);
		fc_tuples[fc_i] = (IndexTuple) fc_ptr;
		fc_ptr += IndexTupleSize((IndexTuple) fc_ptr);
	}
	Assert(fc_ptr - fc_begin == fc_len);

	return fc_tuples;
}

static void fc_gistRedoPageSplitRecord(XLogReaderState *fc_record)
{
	XLogRecPtr	fc_lsn = fc_record->EndRecPtr;
	gistxlogPageSplit *fc_xldata = (gistxlogPageSplit *) XLogRecGetData(fc_record);
	Buffer		fc_firstbuffer = InvalidBuffer;
	Buffer		fc_buffer;
	Page		fc_page;
	int			fc_i;
	bool		fc_isrootsplit = false;

	/*
	 * 我们必须在整个操作过程中保持锁定在第一个列出的页面，
	 * 包括在更新左子页面（如果有）时。我们可以在剩余页面
	 * 被写入后立即解锁，因为没有路径让并发查询在
	 * 首先访问第一个列出的页面之前到达这些页面。
	 */

	/* 循环遍历所有页面 */
	for (fc_i = 0; fc_i < fc_xldata->npage; fc_i++)
	{
		int			fc_flags;
		char	   *fc_data;
		Size		fc_datalen;
		int			fc_num;
		BlockNumber fc_blkno;
		IndexTuple *fc_tuples;

		XLogRecGetBlockTag(fc_record, fc_i + 1, NULL, NULL, &fc_blkno);
		if (fc_blkno == GIST_ROOT_BLKNO)
		{
			Assert(fc_i == 0);
			fc_isrootsplit = true;
		}

		fc_buffer = XLogInitBufferForRedo(fc_record, fc_i + 1);
		fc_page = (Page) BufferGetPage(fc_buffer);
		fc_data = XLogRecGetBlockData(fc_record, fc_i + 1, &fc_datalen);

		fc_tuples = fc_decodePageSplitRecord(fc_data, fc_datalen, &fc_num);

		/* 好的，清除缓冲区 */
		if (fc_xldata->origleaf && fc_blkno != GIST_ROOT_BLKNO)
			fc_flags = F_LEAF;
		else
			fc_flags = 0;
		GISTInitBuffer(fc_buffer, fc_flags);

		/* 并填充它 */
		gistfillbuffer(fc_page, fc_tuples, fc_num, FirstOffsetNumber);

		if (fc_blkno == GIST_ROOT_BLKNO)
		{
			GistPageGetOpaque(fc_page)->rightlink = InvalidBlockNumber;
			GistPageSetNSN(fc_page, fc_xldata->orignsn);
			GistClearFollowRight(fc_page);
		}
		else
		{
			if (fc_i < fc_xldata->npage - 1)
			{
				BlockNumber fc_nextblkno;

				XLogRecGetBlockTag(fc_record, fc_i + 2, NULL, NULL, &fc_nextblkno);
				GistPageGetOpaque(fc_page)->rightlink = fc_nextblkno;
			}
			else
				GistPageGetOpaque(fc_page)->rightlink = fc_xldata->origrlink;
			GistPageSetNSN(fc_page, fc_xldata->orignsn);
			if (fc_i < fc_xldata->npage - 1 && !fc_isrootsplit &&
				fc_xldata->markfollowright)
				GistMarkFollowRight(fc_page);
			else
				GistClearFollowRight(fc_page);
		}

		PageSetLSN(fc_page, fc_lsn);
		MarkBufferDirty(fc_buffer);

		if (fc_i == 0)
			fc_firstbuffer = fc_buffer;
		else
			UnlockReleaseBuffer(fc_buffer);
	}

	/* 修正左子页面的 follow-right 数据（如果有） */
	if (XLogRecHasBlockRef(fc_record, 0))
		fc_gistRedoClearFollowRight(fc_record, 0);

	/* 最后，释放第一个页面的锁 */
	UnlockReleaseBuffer(fc_firstbuffer);
}

/* 重做页面删除 */
static void fc_gistRedoPageDelete(XLogReaderState *fc_record)
{
	XLogRecPtr	fc_lsn = fc_record->EndRecPtr;
	gistxlogPageDelete *fc_xldata = (gistxlogPageDelete *) XLogRecGetData(fc_record);
	Buffer		fc_parentBuffer;
	Buffer		fc_leafBuffer;

	if (XLogReadBufferForRedo(fc_record, 0, &fc_leafBuffer) == BLK_NEEDS_REDO)
	{
		Page		fc_page = (Page) BufferGetPage(fc_leafBuffer);

		GistPageSetDeleted(fc_page, fc_xldata->deleteXid);

		PageSetLSN(fc_page, fc_lsn);
		MarkBufferDirty(fc_leafBuffer);
	}

	if (XLogReadBufferForRedo(fc_record, 1, &fc_parentBuffer) == BLK_NEEDS_REDO)
	{
		Page		fc_page = (Page) BufferGetPage(fc_parentBuffer);

		PageIndexTupleDelete(fc_page, fc_xldata->downlinkOffset);

		PageSetLSN(fc_page, fc_lsn);
		MarkBufferDirty(fc_parentBuffer);
	}

	if (BufferIsValid(fc_parentBuffer))
		UnlockReleaseBuffer(fc_parentBuffer);
	if (BufferIsValid(fc_leafBuffer))
		UnlockReleaseBuffer(fc_leafBuffer);
}

static void fc_gistRedoPageReuse(XLogReaderState *fc_record)
{
	gistxlogPageReuse *fc_xlrec = (gistxlogPageReuse *) XLogRecGetData(fc_record);

	/*
	 * PAGE_REUSE 记录存在的目的是在我们通过 FSM 重用索引中的页面时提供冲突点。它们仅此而已。
	 *
	 * latestRemovedXid 是页面的 deleteXid。
	 * gistPageRecyclable() 中的 GlobalVisCheckRemovableFullXid(deleteXid) 测试在概念上与 GetConflictingVirtualXIDs() 中的 PGPROC->xmin > limitXmin 测试相对应。因此，一个 XID 值在主节点和备用节点上实现了相同的排除效果。
	 */
	if (InHotStandby)
		ResolveRecoveryConflictWithSnapshotFullXid(fc_xlrec->latestRemovedFullXid,
												   fc_xlrec->node);
}

void gist_redo(XLogReaderState *fc_record)
{
	uint8		fc_info = XLogRecGetInfo(fc_record) & ~XLR_INFO_MASK;
	MemoryContext fc_oldCxt;

	/*
	 * GiST 索引不需要任何冲突处理。注意：如果我们将来在 b-tree 中实现类似的优化，并在 VACUUM 之外移除已被杀死的元组，我们需要在这里处理。
	 */

	fc_oldCxt = MemoryContextSwitchTo(opCtx);
	switch (fc_info)
	{
		case XLOG_GIST_PAGE_UPDATE:
			fc_gistRedoPageUpdateRecord(fc_record);
			break;
		case XLOG_GIST_DELETE:
			fc_gistRedoDeleteRecord(fc_record);
			break;
		case XLOG_GIST_PAGE_REUSE:
			fc_gistRedoPageReuse(fc_record);
			break;
		case XLOG_GIST_PAGE_SPLIT:
			fc_gistRedoPageSplitRecord(fc_record);
			break;
		case XLOG_GIST_PAGE_DELETE:
			fc_gistRedoPageDelete(fc_record);
			break;
		case XLOG_GIST_ASSIGN_LSN:
			/* nop. 请参见 gistGetFakeLSN()。 */
			break;
		default:
			elog(PANIC, "gist_redo: unknown op code %u", fc_info);
	}

	MemoryContextSwitchTo(fc_oldCxt);
	MemoryContextReset(opCtx);
}

void gist_xlog_startup(void)
{
	opCtx = createTempGistContext();
}

void gist_xlog_cleanup(void)
{
	MemoryContextDelete(opCtx);
}

/*
 * 在对 Gist 页面运行一致性检查之前进行掩码处理。
 */
void gist_mask(char *fc_pagedata, BlockNumber fc_blkno)
{
	Page		fc_page = (Page) fc_pagedata;

	mask_page_lsn_and_checksum(fc_page);

	mask_page_hint_bits(fc_page);
	mask_unused_space(fc_page);

	/*
	 * NSN 只是一个特殊用途的 LSN。因此，出于与 mask_page_lsn_and_checksum 相同的原因来对其进行掩码处理。
	 */
	GistPageSetNSN(fc_page, (uint64) MASK_MARKER);

	/*
	 * 我们在写入 WAL 记录后更新左子节点上的 F_FOLLOW_RIGHT 标志。因此，请对该标志进行掩码处理。有关详细信息，请参见 gistplacetopage()。
	 */
	GistMarkFollowRight(fc_page);

	if (GistPageIsLeaf(fc_page))
	{
		/*
		 * 在 gist 叶子页面中，可以在不发出任何 WAL 记录的情况下修改 LP_FLAGS。因此，请对行指针标志进行掩码处理。有关详细信息，请参见 gistkillitems()。
		 */
		mask_lp_flags(fc_page);
	}

	/*
	 * 在 gist 重做期间，我们从不将页面标记为垃圾。因此，请对其进行掩码处理以忽略任何差异。
	 */
	GistClearPageHasGarbage(fc_page);
}

/*
 * 写入一个页面拆分的 WAL 记录。
 */
XLogRecPtr gistXLogSplit(bool fc_page_is_leaf,
			  SplitedPageLayout *fc_dist,
			  BlockNumber fc_origrlink, GistNSN fc_orignsn,
			  Buffer fc_leftchildbuf, bool fc_markfollowright)
{
	gistxlogPageSplit fc_xlrec;
	SplitedPageLayout *fc_ptr;
	int			fc_npage = 0;
	XLogRecPtr	fc_recptr;
	int			fc_i;

	for (fc_ptr = fc_dist; fc_ptr; fc_ptr = fc_ptr->next)
		fc_npage++;

	fc_xlrec.origrlink = fc_origrlink;
	fc_xlrec.orignsn = fc_orignsn;
	fc_xlrec.origleaf = fc_page_is_leaf;
	fc_xlrec.npage = (uint16) fc_npage;
	fc_xlrec.markfollowright = fc_markfollowright;

	XLogBeginInsert();

	/*
	 * 包括子缓冲区的完整页面图像。（仅在自子页面拆分以来发生检查点时才需要）
	 */
	if (BufferIsValid(fc_leftchildbuf))
		XLogRegisterBuffer(0, fc_leftchildbuf, REGBUF_STANDARD);

	/*
	 * 注意：我们注册了很多数据。调用者必须已经调用 XLogEnsureRecordSpace() 进行准备。我们不能在这里执行此操作，因为我们已经处于关键区。如果在此处更改缓冲区或数据注册的数量，请确保相应地修改 XLogEnsureRecordSpace() 调用！
	 */
	XLogRegisterData((char *) &fc_xlrec, sizeof(gistxlogPageSplit));

	fc_i = 1;
	for (fc_ptr = fc_dist; fc_ptr; fc_ptr = fc_ptr->next)
	{
		XLogRegisterBuffer(fc_i, fc_ptr->buffer, REGBUF_WILL_INIT);
		XLogRegisterBufData(fc_i, (char *) &(fc_ptr->block.num), sizeof(int));
		XLogRegisterBufData(fc_i, (char *) fc_ptr->list, fc_ptr->lenlist);
		fc_i++;
	}

	fc_recptr = XLogInsert(RM_GIST_ID, XLOG_GIST_PAGE_SPLIT);

	return fc_recptr;
}

/*
 * 写入描述页面删除的 XLOG 记录。这还包括从父页面移除下链接。
 */
XLogRecPtr gistXLogPageDelete(Buffer fc_buffer, FullTransactionId fc_xid,
				   Buffer fc_parentBuffer, OffsetNumber fc_downlinkOffset)
{
	gistxlogPageDelete fc_xlrec;
	XLogRecPtr	fc_recptr;

	fc_xlrec.deleteXid = fc_xid;
	fc_xlrec.downlinkOffset = fc_downlinkOffset;

	XLogBeginInsert();
	XLogRegisterData((char *) &fc_xlrec, SizeOfGistxlogPageDelete);

	XLogRegisterBuffer(0, fc_buffer, REGBUF_STANDARD);
	XLogRegisterBuffer(1, fc_parentBuffer, REGBUF_STANDARD);

	fc_recptr = XLogInsert(RM_GIST_ID, XLOG_GIST_PAGE_DELETE);

	return fc_recptr;
}

/*
 * 写入空的 XLOG 记录以分配一个独特的 LSN。
 */
XLogRecPtr gistXLogAssignLSN(void)
{
	int			fc_dummy = 0;

	/*
	 * 除 SWITCH_WAL 外的记录必须有内容。我们使用整数 0 来遵循这个限制。
	 */
	XLogBeginInsert();
	XLogSetRecordFlags(XLOG_MARK_UNIMPORTANT);
	XLogRegisterData((char *) &fc_dummy, sizeof(fc_dummy));
	return XLogInsert(RM_GIST_ID, XLOG_GIST_ASSIGN_LSN);
}

/*
 * 写入关于重用已删除页面的 XLOG 记录。
 */
void gistXLogPageReuse(Relation fc_rel, BlockNumber fc_blkno, FullTransactionId fc_latestRemovedXid)
{
	gistxlogPageReuse fc_xlrec_reuse;

	/*
	 * 注意，我们没有将缓冲区与记录注册，因为此操作不会修改页面。此记录仅存在于为热备用提供冲突点。
	 */

	/* XLOG 相关内容 */
	fc_xlrec_reuse.node = fc_rel->rd_node;
	fc_xlrec_reuse.block = fc_blkno;
	fc_xlrec_reuse.latestRemovedFullXid = fc_latestRemovedXid;

	XLogBeginInsert();
	XLogRegisterData((char *) &fc_xlrec_reuse, SizeOfGistxlogPageReuse);

	XLogInsert(RM_GIST_ID, XLOG_GIST_PAGE_REUSE);
}

/*
 * 写入描述页面更新的 XLOG 记录。更新可以包括在单个索引页面上的任何数量的删除和/或插入元组。
 *
 * 如果此更新为拆分页面插入了下链接，还要记录子页面上的 F_FOLLOW_RIGHT 标志被清除和 NSN 被设置。
 *
 * 请注意，todelete 数组和元组都被标记为属于目标缓冲区；如果 XLogInsert 决定记录整个缓冲区内容，它们不需要存储在 XLOG 中。
 */
XLogRecPtr gistXLogUpdate(Buffer fc_buffer,
			   OffsetNumber *fc_todelete, int fc_ntodelete,
			   IndexTuple *fc_itup, int fc_ituplen,
			   Buffer fc_leftchildbuf)
{
	gistxlogPageUpdate fc_xlrec;
	int			fc_i;
	XLogRecPtr	fc_recptr;

	fc_xlrec.ntodelete = fc_ntodelete;
	fc_xlrec.ntoinsert = fc_ituplen;

	XLogBeginInsert();
	XLogRegisterData((char *) &fc_xlrec, sizeof(gistxlogPageUpdate));

	XLogRegisterBuffer(0, fc_buffer, REGBUF_STANDARD);
	XLogRegisterBufData(0, (char *) fc_todelete, sizeof(OffsetNumber) * fc_ntodelete);

	/* new tuples */
	for (fc_i = 0; fc_i < fc_ituplen; fc_i++)
		XLogRegisterBufData(0, (char *) (fc_itup[fc_i]), IndexTupleSize(fc_itup[fc_i]));

	/*
	 * 包括子缓冲区的完整页面图像。（仅在自子页面拆分以来发生检查点时才需要）
	 */
	if (BufferIsValid(fc_leftchildbuf))
		XLogRegisterBuffer(1, fc_leftchildbuf, REGBUF_STANDARD);

	fc_recptr = XLogInsert(RM_GIST_ID, XLOG_GIST_PAGE_UPDATE);

	return fc_recptr;
}

/*
 * 写入 XLOG 记录，描述在新元组插入期间标记为 DEAD 的叶索引元组的删除。
 * 一个人可能认为这个案例已经被 gistXLogUpdate() 覆盖。但索引元组的删除
 * 可能与备用查询发生冲突，需要特殊处理。
 */
XLogRecPtr gistXLogDelete(Buffer fc_buffer, OffsetNumber *fc_todelete, int fc_ntodelete,
			   TransactionId fc_latestRemovedXid)
{
	gistxlogDelete fc_xlrec;
	XLogRecPtr	fc_recptr;

	fc_xlrec.latestRemovedXid = fc_latestRemovedXid;
	fc_xlrec.ntodelete = fc_ntodelete;

	XLogBeginInsert();
	XLogRegisterData((char *) &fc_xlrec, SizeOfGistxlogDelete);

	/*
	 * 无论我们是否存储整个缓冲区，我们都需要目标偏移量数组，以便
	 * 让我们能够找到备用服务器上的 latestRemovedXid。
	 */
	XLogRegisterData((char *) fc_todelete, fc_ntodelete * sizeof(OffsetNumber));

	XLogRegisterBuffer(0, fc_buffer, REGBUF_STANDARD);

	fc_recptr = XLogInsert(RM_GIST_ID, XLOG_GIST_DELETE);

	return fc_recptr;
}
